﻿// -------------------------------------------------------------------------------------------------
// <copyright file="TVSeriesFactory.cs" company="HeterogeneousComputing.com">
// Creative Commons Attribution-ShareAlike 3.0 Unported License
// </copyright>
//
// <author>Jock</author>
//
// <date>04/08/2011</date>
//
// <summary>Implements the tv series factory class</summary>
// -------------------------------------------------------------------------------------------------
namespace TheTVDBScraper
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Runtime.Serialization; 
    using System.Text;
    using System.Xml;
    using System.Xml.Linq;
    using NLog;

    /// <summary>
    /// Factory class that parses the results from TheTVDB
    /// and builds a List of TVSeries from the output.
    /// </summary>
    public class TVSeriesFactory
    {
        #region Fields

            /// <summary> Logging Framework. </summary>
            private static Logger logger = NLog.LogManager.GetCurrentClassLogger();

            /// <summary> Temporary List of Actors stored between the parsing of ZIP files entries.  </summary>
            private static Dictionary<string, Actor> actorDictionary = new Dictionary<string, Actor>();

            /// <summary> Temporary List of Directors stored between the parsing of ZIP files entries.  </summary>
            private static Dictionary<string, Director> directorDictionary = new Dictionary<string, Director>();

            /// <summary> Temporary List of TVSEries Writers stored between the parsing of ZIP files entries.  </summary>
            private static Dictionary<string, Writer> writerDictionary = new Dictionary<string, Writer>();

        #endregion Fields

        #region Internal Operations
            /// <summary>
            /// Operation that parses the XML results of the WebClient call
            /// to TheTVDB. Returns a List of TVSeries.
            /// </summary>
            /// <param name="xmlResults">
            /// A string containing the XML results from TheTVDB.
            /// </param>
            /// <returns>
            /// Returns a List of TVSeries parsed out of the supplied XML.
            /// </returns>
            internal static List<TVSeries> Parse(string xmlResults)
            {
                #region Logging
                    if (logger.IsDebugEnabled)
                    {
                        logger.Debug("Entering TVSeriesFactory.Parse(\"{0}\")", xmlResults);
                    }
                #endregion Logging

                List<TVSeries> results = new List<TVSeries>();
                XElement resultsDoc = XElement.Load(new StringReader(xmlResults));

                IEnumerable<XElement> seriesResults =
                    from item in resultsDoc.Descendants("Series")
                        select item;

                foreach (var series in seriesResults)
                {
                    TVSeries tvseries = new TVSeries();
                    tvseries.Id = series.Element("id").Value;
                    tvseries.SeriesId = series.Element("seriesid").Value;
                    tvseries.SeriesName = series.Element("SeriesName").Value;

                    #region Logging
                        if (logger.IsDebugEnabled)
                        {
                            logger.Debug("TVSeries Found : Id : {0}, Name : {1}, Language : {2}", tvseries.Id, tvseries.SeriesName, tvseries.Language);
                        }
                    #endregion Logging

                    tvseries.Overview = (string)series.Element("Overview") ?? null;
                    tvseries.Language = series.Element("language").Value;
                    tvseries.IMDBId = (string)series.Element("IMDB_ID") ?? null;
                    tvseries.Banner = (string)series.Element("banner") ?? null;
                    tvseries.Zap2ItId = (string)series.Element("zap2it_id") ?? null;
                    tvseries.FirstAired = (string)series.Element("FirstAired") ?? null;
                    results.Add(tvseries);
                }

                #region Logging
                    if (logger.IsDebugEnabled)
                    {
                        logger.Debug("Exiting TVSeriesFactory.Parse()");
                    }
                #endregion Logging

                return results;
            }

            /// <summary>
            /// Operation that parses the XML results of the WebClient call
            /// to TheTVDB. Returns a TVSeries.
            /// </summary>
            /// <param name="tvseries">
            /// The TVSeries Object to which we want to enrich with additional
            /// data. 
            /// </param>
            /// <param name="xmlFileDictionary">
            /// A dictionary containing the three xml strings for TVSeries,
            /// Banners and Actors from TheTVDB.
            /// </param>
            internal void ParseDetail(TVSeries tvseries,  Dictionary<string, string> xmlFileDictionary)
            {
                #region Logging
                    if (logger.IsDebugEnabled)
                    {
                        logger.Debug("Entering TVSeriesFactory.ParseDetail(\"{0}\")", tvseries.SeriesName);
                    }
                #endregion

                this.ParseTVSeriesXML(tvseries, xmlFileDictionary);
                this.ParseActorXml(tvseries, xmlFileDictionary);
                #region Logging
                    if (logger.IsDebugEnabled)
                    {
                        logger.Debug("Exiting TVSeriesFactory.ParseDetail()");
                    }
                #endregion
            }

            /// -------------------------------------------------------------------------------------------------
            /// <summary>   Parse TVSeries xml from the language.xml file. </summary>
            /// <remarks>    The method author was Jock, on the 08/08/2011. </remarks>
            /// <exception cref="RequestElementNotInDictionaryException"> Thrown when
            /// requestelementnotindictionary. </exception>
            /// <param name="tvseries">          The TVSeries Object to which we want to enrich with
            /// additional data. </param>
            /// <param name="xmlFileDictionary">    Dictionary of xml files. </param>
            /// <returns>   . </returns>
            /// -------------------------------------------------------------------------------------------------
            private string ParseTVSeriesXML(TVSeries tvseries, Dictionary<string, string> xmlFileDictionary)

            {
                string tvSeriesXml;
                if (xmlFileDictionary.TryGetValue(tvseries.Language + ".xml", out tvSeriesXml))
                {
                    XElement tvSeriesDoc = XElement.Load(new StringReader(tvSeriesXml));
                    this.ParseTVSeries(tvseries, tvSeriesDoc);
                }
                else
                {
                    logger.Error("TVSeries {0}.xml not found in ZIP file.", tvseries.Language);
                    throw new RequestElementNotInDictionaryException("TVSeries Language "
                        + tvseries.Language + ".xml not found in the downloaded ZIP file.");
                }

                return tvSeriesXml;
                
            }

            /// -------------------------------------------------------------------------------------------------
            /// <summary>   Parse TVSeries XML. </summary>
            /// <remarks>    The method author was Jock, on the 08/08/2011. </remarks>
            /// <param name="tvseries">   The TVSeries Object to which we want to enrich with additional data. </param>
            /// <param name="resultsDoc">   The XML Document Series tag. </param>
            /// -------------------------------------------------------------------------------------------------
            private void ParseTVSeries(TVSeries tvseries, XElement resultsDoc)
            {
                IEnumerable<XElement> seriesResults =
                    from series in resultsDoc.Descendants("Series")
                    select series;
                foreach (var series in seriesResults)
                {
                    tvseries.AirsDayOfWeek = (string)series.Element("Airs_DayOfWeek") ?? null;
                    tvseries.AirsTime = (string)series.Element("Airs_Time") ?? null;
                    string actorsList = (string)series.Element("Actors") ?? null;
                    if (actorsList != null)
                    {
                        string[] actorArray = actorsList.Split('|');
                        foreach (string actor in actorArray)
                        {
                            if (actor != string.Empty)
                            {
                                if (!actorDictionary.ContainsKey(actor))
                                {
                                    actorDictionary.Add(actor, new Actor(actor));
                                }
                            }
                        }
                    }

                    this.CreateSeasons(tvseries, resultsDoc);
                    this.CreateEpisodes(tvseries, resultsDoc);
                }
            }

            /// <summary>
            /// Operation that parses the XML results of the WebClient call
            /// to TheTVDB. Returns a TVSeries in XML format. Delegates
            /// all but XMLSerialisation to ParseDetail.
            /// </summary>
            /// <param name="tvseries">
            /// The TVSeries Object to which we want to enrich with additional
            /// data. 
            /// </param>
            /// <param name="xmlResults">
            /// A string containing the XML results from TheTVDB.
            /// </param>
            /// <returns>
            /// An XML Representation of the TVSeries.
            /// </returns>
            internal string ParseDetailXML(TVSeries tvseries, Dictionary<string, string> xmlFileDictionary)
            {
                DataContractSerializer serializer =
                    new DataContractSerializer(typeof(TVSeries));
                var output = new StringWriter();
                this.ParseDetail(tvseries, xmlFileDictionary);
                using (var writer = new XmlTextWriter(output) { Formatting = Formatting.Indented })
                {
                    serializer.WriteObject(writer, tvseries);
                }

                return output.GetStringBuilder().ToString();
            }

            /// -------------------------------------------------------------------------------------------------
            /// <summary>   Creates a new TVEpisode. </summary>
            /// <remarks>    The method author was Jock, on the 05/08/2011. </remarks>
            /// <param name="tvseries"> The TVSeries Object to which we want to enrich with additional data. </param>
            /// <param name="resultsDoc">   The XML Document Series tag. </param>
            /// -------------------------------------------------------------------------------------------------
            private void CreateEpisodes(TVSeries tvseries, XElement resultsDoc)
            {
                #region Logging
                    if (logger.IsDebugEnabled)
                    {
                        logger.Debug("Entering TVSeriesFactory.CreateEpisodes(\"{0}\", \"{1}\")", tvseries.SeriesName, resultsDoc);
                    }
                #endregion Logging

                IEnumerable<XElement> episodesResults =
                    from episode in resultsDoc.Descendants("Episode")
                    orderby (string)episode.Element("id")
                    select episode;

                foreach (var episode in episodesResults)
                {
                    TVSeason seasonForEpisode = tvseries.GetSeason(episode.Element("seasonid").Value ?? null);
                    if (seasonForEpisode == null)
                    {
                        // TODO:
                    }
                    else
                    {
                        TVEpisode newEpisode = new TVEpisode(episode.Element("id").Value);
                        newEpisode.EpisodeName = (string)episode.Element("EpisodeName");
                        newEpisode.EpisodeNumber = (string)episode.Element("EpisodeNumber");
                        newEpisode.Overview = (string)episode.Element("Overview");
                        /*
                        newEpisode.FirstAired = (string)episode.Element("FirstAired") ?? null;
                        newEpisode.Language = (string)episode.Element("Language");
                        newEpisode.ProductionCode = (string)episode.Element("ProductionCode" ?? null);
                        newEpisode.Rating = (string)episode.Element("Rating" ?? null);
                        newEpisode.RatingCount = (string)episode.Element("RatingCount" ?? null);
                        newEpisode.AirsBeforeSeason = (string)episode.Element("AirsBeforeSeason" ?? null);
                        newEpisode.AirsAfterSeason = (string)episode.Element("airsafter_season" ?? null);
                        newEpisode.AirsBeforeEpisode = (string)episode.Element("airsbefore_episode" ?? null);
                        newEpisode.LastUpdated = (string)episode.Element("lastupdated" ?? null);
                        newEpisode.Filename = (string)episode.Element("filename" ?? null);
                        */
                        string directorsList = (string)episode.Element("Director") ?? null;
                        this.AddJobRole(directorsList, ref newEpisode, new Director(), ref directorDictionary);

                        string writersList = (string)episode.Element("Writer") ?? null;
                        this.AddJobRole(writersList, ref newEpisode, new Writer(), ref writerDictionary);

                        string guestStarsList = (string)episode.Element("GuestStars") ?? null;
                        this.AddJobRole(guestStarsList, ref newEpisode, new Actor(), ref actorDictionary);

                        #region Logging
                            if (logger.IsDebugEnabled)
                            {
                                logger.Debug("Entering TVSeriesFactory.CreateEpisodes#TVEpisode Id :\"{0}\", Name : \"{1}\")", newEpisode.Id, newEpisode.EpisodeName);
                            }
                        #endregion Logging

                        // Combined_episodenumber
                        // Combined_season
                        // DVD_chapter
                        // DVD_discid
                        // DVD_episodenumber
                        // DVD_season
                        // EpImgFlag
                        // EpisodeNumber
                        // IMDB_ID
                        // SeasonNumber
                        // absolute_number
                        // seasonid
                        // seriesid
                        seasonForEpisode.AddEpisode(newEpisode);

                        #region Logging
                            if (logger.IsDebugEnabled)
                            {
                                logger.Debug("Exiting TVSeriesFactory.CreateEpisodes#TVEpisode Id :\"{0}\", Name : \"{1}\")", newEpisode.Id, newEpisode.EpisodeName);
                            }
                        #endregion Logging
                    }
                }

                #region Logging
                    if (logger.IsDebugEnabled)
                    {
                        logger.Debug("Exiting TVSeriesFactory.CreateEpisodes()");
                    }
                #endregion Logging
            }

            /// -------------------------------------------------------------------------------------------------
            /// <summary>   Adds a job role R to an Entity supporting the IJobRoleCapable interface. </summary>
            /// <remarks>    The method author was Jock, on the 05/08/2011. </remarks>
            /// <typeparam name="E">    Type of the Entity supplied which must support IJobRoleCapable. </typeparam>
            /// <typeparam name="R">    Generic type parameter which is a Class that is a subclass of JobRole. </typeparam>
            /// <typeparam name="D">    Generic type parameter which is a Dictionary of type Dictionary&lt;string, R&gt;. </typeparam>
            /// <param name="stringToSplit">    The string to split into it's component parts. </param>
            /// <param name="entity">           The entity we will add the new job role to. </param>
            /// <param name="jobRole">          The job role we will instantiate from each string component. </param>
            /// <param name="dictionary">       [in,out] The dictionary in which we will store the string component and JobRole. </param>
            /// -------------------------------------------------------------------------------------------------
            private void AddJobRole<E, R, D>(string stringToSplit, ref E entity, R jobRole, ref D dictionary)
                where E : IJobRoleCapable
                where R : JobRole,  new()
                where D : Dictionary<string, R>
            {
                if (stringToSplit != null)
                {
                    string[] stringArray = stringToSplit.Split('|');
                    foreach (string str in stringArray)
                    {
                        R newJobRole;
                        if (str.Trim() != string.Empty)
                        {
                            if (!dictionary.TryGetValue(str, out newJobRole))
                            {
                                newJobRole = new R();
                                newJobRole.Person = new Person(str);
                                dictionary.Add(str, newJobRole);
                            }

                            entity.Add<R>(newJobRole);
                        }
                    }
                }
            }

            /// -------------------------------------------------------------------------------------------------
            /// <summary>   Creates the TVSeasons. </summary>
            /// <remarks>    The method author was Jock, on the 04/08/2011. </remarks>
            /// <param name="tvseries">   The TVSeries Object to which we want to enrich with additional data. </param>
            /// <param name="resultsDoc">   The XML results document. </param>
            /// -------------------------------------------------------------------------------------------------
            private void CreateSeasons(TVSeries tvseries, XElement resultsDoc)
            {
                #region Logging
                    if (logger.IsDebugEnabled)
                    {
                        logger.Debug("Entering TVSeriesFactory.CreateEpisodes(\"{0}\", \"{1}\")", tvseries.SeriesName, resultsDoc);
                    }
                #endregion Logging

                var dvds = from episode in resultsDoc.Descendants("Episode")
                           select episode.Element("seasonid").Value;
                foreach (var season in dvds)
                {
                    tvseries.AddSeason(new TVSeason(season));
                }

                #region Logging
                    if (logger.IsDebugEnabled)
                    {
                        logger.Debug("Exiting TVSeriesFactory.CreateSeasons()");
                    }
                #endregion Logging
            }

            /// -------------------------------------------------------------------------------------------------
            /// <summary>   Parse actor xml. </summary>
            /// <remarks>    The method author was Jock, on the 08/08/2011. </remarks>
            /// <exception cref="RequestElementNotInDictionaryException"> Thrown when
            /// requestelementnotindictionary. </exception>
            /// <param name="series">               The series for which to retrieve data. </param>
            /// <param name="xmlFileDictionary">    Dictionary of xml files. </param>
            /// <returns>   The parsed XML. </returns>
            /// -------------------------------------------------------------------------------------------------
            private string ParseActorXml(TVSeries series, Dictionary<string, string> xmlFileDictionary)
            {
                string actorXml;
                if (xmlFileDictionary.TryGetValue("actors.xml", out actorXml))
                {
                    XElement actorsDoc = XElement.Load(new StringReader(actorXml));
                    this.ParseActorsAndCharacters(series, actorsDoc);
                }
                else
                {
                    logger.Error("TVSeries actors.xml not found in ZIP file.", series.Language);
                    throw new RequestElementNotInDictionaryException("TVSeries Language "
                        + series.Language + " not found in the downloaded ZIP file.");
                }

                return actorXml;
            }

            internal void ParseActorsAndCharacters(TVSeries tvseries, XElement resultsDoc)
            {
                #region Logging
                    if (logger.IsDebugEnabled)
                    {
                        logger.Debug("Entering TVSeriesFactory.ParseActorsAndCharacters(\"{0}\", \"{1}\")", tvseries.SeriesName, resultsDoc);
                    }
                #endregion Logging

                var actors = from actor in resultsDoc.Descendants("Actor")
                             select actor;

                foreach (var actor in actors)
                {
                    string actorName = (string)actor.Element("Name");
                    Actor actorObject;

                    if (TVSeriesFactory.actorDictionary.TryGetValue(actorName, out actorObject))
                    {
                        #region Logging
                            if (logger.IsDebugEnabled)
                            {
                                logger.Debug("TVSeriesFactory.ParseActorsAndCharacters() found Actor {0} in actorDictionary", actorObject.Name);
                            }
                        #endregion Logging
                    }
                    else
                    {
                        #region Logging
                            if (logger.IsDebugEnabled)
                            {
                                logger.Debug("TVSeriesFactory.ParseActorsAndCharacters() not found Actor {0} in actorDictionary, Creating... ", actorName);
                            }
                        #endregion Logging
                        actorObject = new Actor(actorName);
                        TVSeriesFactory.actorDictionary.Add(actorName, actorObject);
                    }
                    
                    actorObject.Id = (string)actor.Element("id");
                    actorObject.ImageURL = (string)actor.Element("Image");

                    TVCharacter newCharacter = new TVCharacter((string)actor.Element("Role"), actorObject);
                    #region Logging
                        if (logger.IsDebugEnabled)
                        {
                            logger.Debug("TVSeriesFactory.ParseActorsAndCharacters() creating TVCharacter {0} using Actor {1}. ", newCharacter.Name, actorObject.Name);
                        }
                    #endregion Logging
                    newCharacter.SortOrder = (string)actor.Element("SortOrder");

                    tvseries.AddCharacter(newCharacter);
                }

                #region Logging
                    if (logger.IsDebugEnabled)
                    {
                        logger.Debug("Exiting TVSeriesFactory.ParseActorsAndCharacters()");
                    }
                #endregion Logging
            }

        #endregion  Internal Operations
    }
}

/// -------------------------------------------------------------------------------------------------
// End of TVSeriesFactory.cs
/// -------------------------------------------------------------------------------------------------
